Skip to content

feat(tbor): implement AesGenerateKey + AesEncryptDecrypt (masked-key AES) - #584

Merged
Vishal Soni (vsonims) merged 1 commit into
mainfrom
tbor/aes-crypto
Aug 1, 2026
Merged

feat(tbor): implement AesGenerateKey + AesEncryptDecrypt (masked-key AES)#584
Vishal Soni (vsonims) merged 1 commit into
mainfrom
tbor/aes-crypto

Conversation

@vsonims

Copy link
Copy Markdown
Collaborator

Summary

Adds the TBOR AES crypto command pair, stacked on the RSA-AES
key-unwrap commands (#583), following the MBOR AES commands but with the
TBOR masked-key (stateless) model — no vault key_id / key_tag.

AesGenerateKey (opcode 0x15)

Generate a fresh random AES key (128 / 192 / 256 bits, non-bulk only,
mirroring MBOR) and return it masked (AEAD-GCM-256) under the
requested scope's masking key. The masked-blob length is fixed by the key
size, so the handler reserves the response slot up front and masks the
generated key straight into it (encoder *_reserve + decode_mut) — no
scratch buffer, no copy.

AesEncryptDecrypt (opcode 0x16)

AES-CBC encrypt or decrypt a host-supplied message with a caller-held
masked AES key. Fully zero-copy:

  • request masked_key is #[tbor(mutable)] → the handler decode_muts
    the request and unmasks the blob in place, using the recovered
    key directly (no blob / key scratch copy);
  • the response msg / iv slots are reserved, and the non-in-place
    aes_cbc_enc_dec reads the request message and writes the ciphertext /
    chaining IV straight into the response (no message copy).

Rejects non-AES key kinds (InvalidKeyType) and direction-permission
mismatches (InvalidPermissions); the recovered key is wiped on every
path (including the unmask-failure path).

AES key import

Already provided by UnwrapKey (0x14, key class Aes) in #583 — an
emu test exercises the UnwrapKeyAesEncryptDecrypt round-trip.

Wiring

Both opcodes are routed through the fw dispatcher, the is_known_opcode
/ is_in_session / needs_session_id_cross_check classifiers, and
op::SessionCtrl::from_tbor_opcode (InSession, CO/CU). Reuses the shared
validate_active_session / resolve_masking_key helpers and the encoder
reserve/fill support from #583.

Tests / validation

  • Emu: keygen round-trip across all sizes/scopes, before-finalize +
    SD-scope + unknown-size rejects; encrypt→decrypt round-trip all sizes,
    CBC chaining IV, tamper + bad-length rejects, and the UnwrapKey→AES
    cross-command round-trip.
  • Full TBOR emu suite green (127 tests), clippy clean, nightly
    fmt + copyright clean, Uno (thumbv7em) build passes.
  • Docs: docs/tbor-ddi/commands/{aes_generate_key,aes_encrypt_decrypt}.md
    • README rows (0x15, 0x16).

@vsonims
Vishal Soni (vsonims) force-pushed the tbor/unwrap-key branch 2 times, most recently from b3e53f6 to 427588b Compare July 19, 2026 03:32
Comment thread ddi/tbor/types/src/aes_encrypt_decrypt.rs Outdated
Comment thread ddi/tbor/types/tests/commands/aes_encrypt_decrypt.rs Dismissed
Comment thread ddi/tbor/types/tests/commands/aes_encrypt_decrypt.rs Dismissed
Comment thread ddi/tbor/types/tests/commands/aes_encrypt_decrypt.rs Dismissed
Comment thread ddi/tbor/types/tests/commands/aes_generate_key.rs
Comment thread ddi/tbor/types/tests/commands/aes_generate_key.rs Outdated

@jaygmsft Jayant Gandhi (jaygmsft) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approving with suggestions

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds TBOR support for masked-key AES operations by introducing two new in-session opcodes: AesGenerateKey (0x15) to generate a random AES key and return it as an AEAD-masked blob, and AesEncryptDecrypt (0x16) to AES-CBC encrypt/decrypt a message using a caller-supplied masked AES key.

Changes:

  • Wire in new AES opcodes through session classification, dispatcher routing, and session-control enforcement.
  • Implement firmware handlers for AES key generation (masked blob output) and AES-CBC encrypt/decrypt (in-place unmask + zero-copy response fill).
  • Add corresponding TBOR wire schemas, host-side wrappers, docs, and emulator integration tests.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
fw/core/lib/src/op.rs Routes new AES opcodes as InSession for session-control enforcement.
fw/core/lib/src/ddi/tbor/mod.rs Adds opcode constants, dispatch wiring, and session classifiers for 0x15/0x16.
fw/core/lib/src/ddi/tbor/aes_generate_key.rs New FW handler to generate random AES key and return an AEAD-masked blob.
fw/core/lib/src/ddi/tbor/aes_encrypt_decrypt.rs New FW handler to unmask AES key in-place and perform AES-CBC into reserved response slots.
fw/core/ddi/tbor/types/src/lib.rs Exposes new AES wire-schema modules from the FW TBOR types crate.
fw/core/ddi/tbor/types/src/aes_generate_key.rs Defines TBOR wire schema + constants for AesGenerateKey.
fw/core/ddi/tbor/types/src/aes_encrypt_decrypt.rs Defines TBOR wire schema + constants for AesEncryptDecrypt.
docs/tbor-ddi/README.md Documents new TBOR command table entries for opcodes 0x15/0x16.
docs/tbor-ddi/commands/aes_generate_key.md Adds command documentation for AesGenerateKey.
docs/tbor-ddi/commands/aes_encrypt_decrypt.md Adds command documentation for AesEncryptDecrypt.
ddi/tbor/types/tests/commands/mod.rs Registers new AES command integration-test modules.
ddi/tbor/types/tests/commands/aes_generate_key.rs Adds emu integration tests for AES key generation across sizes/scopes and reject paths.
ddi/tbor/types/tests/commands/aes_encrypt_decrypt.rs Adds emu integration tests for CBC round-trips, chaining IV, tamper reject, and unwrap→AES interop.
ddi/tbor/types/src/lib.rs Exposes new host-side TBOR wrapper modules.
ddi/tbor/types/src/aes_generate_key.rs Adds host-side request/response wrapper for AesGenerateKey.
ddi/tbor/types/src/aes_encrypt_decrypt.rs Adds host-side request/response wrapper for AesEncryptDecrypt.

Comment thread fw/core/lib/src/ddi/tbor/aes_generate_key.rs Outdated
Comment thread fw/core/lib/src/ddi/tbor/aes_encrypt_decrypt.rs Outdated
Comment thread docs/tbor-ddi/commands/aes_encrypt_decrypt.md
Copilot AI review requested due to automatic review settings July 31, 2026 00:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

fw/core/lib/src/ddi/tbor/aes_generate_key.rs:133

  • resolve_masking_key is called after generating the random AES key. If the scope has no masking key (e.g., SecurityDomain before CreateSD) or Session scope on Uno (where session_masking_key returns UnsupportedCmd), the handler will still burn RNG and allocate key material before failing. Resolving the masking key first matches the doc comment about failing cheaply and avoids unnecessary keygen work.
            // Generate the random AES key into scratch.
            let key_buf = alloc.dma_alloc(key_len)?;
            pal.aes_gen_key(io, key_buf).await?;

            let masking_key = resolve_masking_key(pal, io, scope, sess_id)?;

docs/tbor-ddi/commands/aes_generate_key.md:35

  • This doc implies scope = Session always works, but the firmware's resolve_masking_key notes that per-session masking keys are only provisioned on std/emu today; on Uno (hardware) scope = Session fails with UnsupportedCmd. Adding the same platform note as unwrap_key.md would prevent confusion for hardware-targeting callers.
- `Session` → the per-session masking key (works for any Active session,
  including before `PartFinal`).

docs/tbor-ddi/commands/aes_encrypt_decrypt.md:19

  • The command selects the masking key based on the masked key’s recorded scope, but on Uno (hardware) scope = Session is not currently supported (per session_masking_key / resolve_masking_key), and will fail with UnsupportedCmd. Consider adding a brief platform note here (as done in unwrap_key.md) so callers don’t assume Session-scope masked keys work on hardware.
[`UnwrapKey`](./unwrap_key.md)). The device reads the masked key's scope
from its cleartext, tag-bound metadata to select the masking key, unmasks
the key on-device (verifying the AEAD tag), runs the AES-CBC transform
zero-copy — reading the request message and writing the transformed message

Copilot AI review requested due to automatic review settings July 31, 2026 15:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Comment thread fw/core/lib/src/ddi/tbor/aes_generate_key.rs Outdated
…AES)

Add the TBOR AES crypto command pair, stacked on the RSA-AES key-unwrap
commands, following the MBOR AES commands but with the TBOR masked-key
(stateless) model.

AesGenerateKey (opcode 0x15): generate a fresh random AES key (128 / 192
/ 256 bits) and return it masked (AEAD-GCM-256) under the requested
scope's masking key. Non-bulk sizes only (mirroring MBOR); no vault
`key_id` / `key_tag`. The masked-blob length is fixed by the key size, so
the handler reserves the response slot up front and masks the generated
key straight into it (encoder `*_reserve` + `decode_mut`) — no scratch
buffer and no copy.

AesEncryptDecrypt (opcode 0x16): AES-CBC encrypt or decrypt a
host-supplied message with a caller-held masked AES key. Fully
zero-copy: the request `masked_key` is `#[tbor(mutable)]`, so the handler
`decode_mut`s the request and `unmask`s the blob in place, then the
non-in-place `aes_cbc_enc_dec` reads the request message and writes the
ciphertext / chaining IV straight into the reserved response slots — no
blob / key scratch copy and no message copy. Rejects non-AES key kinds
(`InvalidKeyType`) and direction-permission mismatches
(`InvalidPermissions`); the recovered key is wiped on every path.

AES key import is already provided by `UnwrapKey` (0x14, key class Aes);
an emu test exercises the unwrap -> encrypt/decrypt round-trip.

Reuses the shared session/masking helpers (`validate_active_session`,
`resolve_masking_key`) and the encoder reserve/fill support from the
key-unwrap PR below it in the stack. Wires both opcodes through the fw
dispatcher, the `is_known_opcode` / `is_in_session` /
`needs_session_id_cross_check` classifiers, and
`op::SessionCtrl::from_tbor_opcode`. Adds fw + host wire schemas, command
docs, and emu tests: keygen round-trip across all sizes/scopes,
encrypt->decrypt round-trip, CBC chaining IV, tamper / bad-length
rejects, and the UnwrapKey->AES cross-command round-trip.

The AES type tests follow the api_rev.rs pilot pattern rather than a
file-level `#![cfg(feature = "emu")]`: the module is gated on
`any(emu, mock, sock)` and only the individual `_emu` tests (plus their
FW-handler-only imports) carry `#[cfg(feature = "emu")]`, so the shared
masked-key helpers/constants aren't limited to the emu backend.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0b09e50a-a9be-4bae-b347-d42dc775a258
Copilot AI review requested due to automatic review settings July 31, 2026 15:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (3)

docs/tbor-ddi/commands/aes_encrypt_decrypt.md:73

  • This command can also fail with MaskedKeyDecodeFailed for malformed masked-key metadata (bad magic/version/padding), because the handler calls peek_metadata/unmask from the masked-key AEAD format. The errors table currently lists only AesGcmDecryptTagDoesNotMatch, which covers tag failures but not metadata decode failures.
| `SessionNotFound` | `session_id` does not refer to an allocated slot, or the slot is not `Active` |
| `InvalidArg` | Unknown `op`, IV not exactly 16 bytes, or `msg` empty / not a multiple of 16 / over 1024 bytes |
| `AesGcmDecryptTagDoesNotMatch` | The masked key's AEAD tag failed to verify (tampered or wrong-scope blob) |
| `InvalidKeyType` | The recovered key is not a non-bulk AES key |
| `InvalidPermissions` | The key lacks the permission for the direction (`encrypt` / `decrypt`) |

docs/tbor-ddi/commands/aes_generate_key.md:34

  • The docs state Session scope works for any Active session, but on Uno hardware the per-session masking key is not provisioned yet (see resolve_masking_key's platform note). Consider adding the same platform note used in unwrap_key.md so callers don’t assume scope = Session works on hardware.
- `Session` → the per-session masking key (works for any Active session,
  including before `PartFinal`).

docs/tbor-ddi/commands/aes_encrypt_decrypt.md:23

  • The description implies Session-scoped masked keys are generally usable, but on Uno hardware the per-session masking key is not provisioned yet (see resolve_masking_key platform note). Adding a brief platform note here would prevent callers from assuming scope = Session works on hardware.

This issue also appears on line 69 of the same file.

[`UnwrapKey`](./unwrap_key.md)). The device reads the masked key's scope
from its cleartext, tag-bound metadata to select the masking key, unmasks
the key on-device (verifying the AEAD tag), runs the AES-CBC transform
zero-copy — reading the request message and writing the transformed message
plus the updated chaining IV straight into the response buffer — so the
host can chain subsequent CBC blocks. This is the TBOR analogue of
MBOR `AesEncryptDecrypt`, keyed by a masked blob rather than a vault
`key_id`. Nothing is persisted and the recovered key is wiped.

@vsonims
Vishal Soni (vsonims) added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit 79b2550 Aug 1, 2026
30 checks passed
@jaygmsft
Jayant Gandhi (jaygmsft) deleted the tbor/aes-crypto branch August 4, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants